// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2024-2025, The OpenROAD Authors

#include "writer.h"

#include <string>

namespace upf {

UPFWriter::UPFWriter(odb::dbBlock* block, utl::Logger* logger)
    : block_(block), logger_(logger)
{
}

void UPFWriter::write(const std::string& file)
{
  stream_.open(file);
  if (!stream_) {
    logger_->error(utl::UPF, 72, "Unable to open: {}", file);
  }

  writeHeader();

  writeTitle("Power domains");
  for (auto* domain : block_->getPowerDomains()) {
    writeDomain(domain);
  }

  writeTitle("Logic ports");
  for (auto* port : block_->getLogicPorts()) {
    writePort(port);
  }

  writeTitle("Power switches");
  for (auto* ps : block_->getPowerSwitches()) {
    writePowerSwitch(ps);
  }

  writeTitle("Isolations");
  for (auto* iso : block_->getIsolations()) {
    writeIsolation(iso);
  }

  writeTitle("Level shifters");
  for (auto* ls : block_->getLevelShifters()) {
    writeLevelShifter(ls);
  }
}

void UPFWriter::writeHeader()
{
  stream_ << "# UPF generated by OpenROAD" << std::endl;
}

void UPFWriter::writeTitle(const std::string& title)
{
  const std::size_t title_len = title.size();

  const std::string title_prefix = fmt::format("{:#>{}}", "", title_len + 2);
  stream_ << std::endl;
  stream_ << title_prefix << std::endl;
  stream_ << "# " << title << std::endl;
  stream_ << title_prefix << std::endl;
}

void UPFWriter::writeDomain(odb::dbPowerDomain* domain)
{
  stream_ << "create_power_domain " << domain->getName();

  if (domain->isTop()) {
    stream_ << "  -elements {.}";
  } else {
    if (!domain->getElements().empty()) {
      lineContinue();
      stream_ << "  -elements {";
      writeList(domain->getElements());
      stream_ << "}";
    }
  }

  stream_ << std::endl;
}

void UPFWriter::writePort(odb::dbLogicPort* port)
{
  stream_ << "create_logic_port " << port->getName();
  lineContinue();
  stream_ << "  -direction " << port->getDirection();
  stream_ << std::endl;
}

void UPFWriter::writePowerSwitch(odb::dbPowerSwitch* ps)
{
  stream_ << "create_power_switch " << ps->getName();
  const auto out_port = ps->getOutputSupplyPort();
  if (!out_port.port_name.empty()) {
    lineContinue();
    stream_ << "  -output_supply_port {";

    ArgList list;
    list.push_back(out_port.port_name);
    if (!out_port.supply_net_name.empty()) {
      list.push_back(out_port.supply_net_name);
    }
    writeList(list);

    stream_ << "}";
  }
  for (const auto& port : ps->getInputSupplyPorts()) {
    lineContinue();
    stream_ << "  -input_supply_port {";

    ArgList list;
    list.push_back(port.port_name);
    if (!port.supply_net_name.empty()) {
      list.push_back(port.supply_net_name);
    }
    writeList(list);

    stream_ << "}";
  }
  for (const auto& port : ps->getControlPorts()) {
    lineContinue();
    stream_ << "  -control_port {";

    ArgList list;
    list.push_back(port.port_name);
    if (!port.net_name.empty()) {
      list.push_back(port.net_name);
    }
    writeList(list);

    stream_ << "}";
  }
  for (const auto& state : ps->getOnStates()) {
    lineContinue();
    stream_ << "  -on_state {";

    ArgList list;
    list.push_back(state.state_name);
    if (!state.input_supply_port.empty()) {
      list.push_back(state.input_supply_port);

      if (!state.boolean_expression.empty()) {
        list.push_back(state.boolean_expression);
      }
    }
    writeList(list);

    stream_ << "}";
  }
  for (const auto& port : ps->getAcknowledgePorts()) {
    lineContinue();
    stream_ << "  -ack_port {";

    ArgList list;
    list.push_back(port.port_name);
    if (!port.net_name.empty()) {
      list.push_back(port.net_name);

      if (!port.boolean_expression.empty()) {
        list.push_back(port.boolean_expression);
      }
    }
    writeList(list);

    stream_ << "}";
  }
  lineContinue();
  stream_ << "  -domain {" << ps->getPowerDomain()->getName() << "}";
  stream_ << std::endl;

  writePowerSwitchPortMap(ps);
}

void UPFWriter::writePowerSwitchPortMap(odb::dbPowerSwitch* ps)
{
  auto* cell = ps->getLibCell();
  if (cell == nullptr) {
    return;
  }

  stream_ << "map_power_switch " << ps->getName();
  lineContinue();
  stream_ << "  -lib_cells {" << cell->getName() << "}";
  lineContinue();
  stream_ << "  -port_map {";
  bool first = true;
  for (const auto& [port, term] : ps->getPortMap()) {
    if (!first) {
      stream_ << " ";
    }
    first = false;

    stream_ << "{";
    ArgList list;
    list.push_back(port);
    list.push_back(term->getName());
    writeList(list);
    stream_ << "}";
  }
  stream_ << "}";
  stream_ << std::endl;
}

void UPFWriter::writeIsolation(odb::dbIsolation* isolation)
{
  stream_ << "set_isolation " << isolation->getName();
  lineContinue();
  stream_ << "  -domain {" << isolation->getPowerDomain()->getName() << "}";
  if (!isolation->getAppliesTo().empty()) {
    lineContinue();
    stream_ << "  -applies_to " << isolation->getAppliesTo();
  }
  if (!isolation->getLocation().empty()) {
    lineContinue();
    stream_ << "  -location " << isolation->getLocation();
  }
  if (!isolation->getClampValue().empty()) {
    lineContinue();
    stream_ << "  -clamp_value " << isolation->getClampValue();
  }
  if (!isolation->getIsolationSignal().empty()) {
    lineContinue();
    stream_ << "  -isolation_signal {" << isolation->getIsolationSignal()
            << "}";
    if (!isolation->getIsolationSense().empty()) {
      lineContinue();
      stream_ << "  -isolation_sense " << isolation->getIsolationSense();
    }
  }
  stream_ << std::endl;
}

void UPFWriter::writeLevelShifter(odb::dbLevelShifter* ls)
{
  stream_ << "set_level_shifter " << ls->getName();
  lineContinue();
  stream_ << "  -domain {" << ls->getDomain()->getName() << "}";
  if (!ls->getElements().empty()) {
    lineContinue();
    stream_ << "  -elements {";
    writeList(ls->getElements());
    stream_ << "}";
  }
  if (!ls->getExcludeElements().empty()) {
    lineContinue();
    stream_ << "  -exclude_elements {";
    writeList(ls->getExcludeElements());
    stream_ << "}";
  }
  if (!ls->getSource().empty()) {
    lineContinue();
    stream_ << "  -source {" << ls->getSource() << "}";
  }
  if (!ls->getSink().empty()) {
    lineContinue();
    stream_ << "  -sink {" << ls->getSink() << "}";
  }
  lineContinue();
  stream_ << "  -use_functional_equivalence ";
  if (ls->isUseFunctionalEquivalence()) {
    stream_ << "TRUE";
  } else {
    stream_ << "FALSE";
  }
  if (!ls->getAppliesTo().empty()) {
    lineContinue();
    stream_ << "  -applies_to " << ls->getAppliesTo();
  }
  if (!ls->getAppliesToBoundary().empty()) {
    lineContinue();
    stream_ << "  -applies_to_boundary " << ls->getAppliesToBoundary();
  }
  if (!ls->getRule().empty()) {
    lineContinue();
    stream_ << "  -rule " << ls->getRule();
  }
  lineContinue();
  stream_ << "  -threshold " << fmt::format("{:.3f}", ls->getThreshold());
  if (ls->isNoShift()) {
    lineContinue();
    stream_ << "  -no_shift";
  }
  if (ls->isForceShift()) {
    lineContinue();
    stream_ << "  -force_shift";
  }
  if (!ls->getLocation().empty()) {
    lineContinue();
    stream_ << "  -location " << ls->getLocation();
  }
  if (!ls->getInputSupply().empty()) {
    lineContinue();
    stream_ << "  -input_supply {" << ls->getInputSupply() << "}";
  }
  if (!ls->getOutputSupply().empty()) {
    lineContinue();
    stream_ << "  -output_supply {" << ls->getOutputSupply() << "}";
  }
  if (!ls->getInternalSupply().empty()) {
    lineContinue();
    stream_ << "  -internal_supply {" << ls->getInternalSupply() << "}";
  }
  if (!ls->getNamePrefix().empty()) {
    lineContinue();
    stream_ << "  -name_prefix {" << ls->getNamePrefix() << "}";
  }
  if (!ls->getNameSuffix().empty()) {
    lineContinue();
    stream_ << "  -name_suffix {" << ls->getNameSuffix() << "}";
  }
  if (!ls->getInstances().empty()) {
    lineContinue();
    stream_ << "  -instance {";
    bool first = true;
    for (const auto& [inst, port] : ls->getInstances()) {
      if (!first) {
        stream_ << " ";
      }
      first = false;

      stream_ << "{";
      ArgList list;
      list.push_back(inst);
      list.push_back(port);
      writeList(list);
      stream_ << "}";
    }
    stream_ << "}";
  }
  stream_ << std::endl;
}

void UPFWriter::writeList(const ArgList& list)
{
  if (list.size() == 1) {
    stream_ << list[0];
    return;
  }

  bool first = true;
  for (const auto& item : list) {
    if (!first) {
      stream_ << " ";
    }
    first = false;

    stream_ << "{" << item << "}";
  }
}

void UPFWriter::lineContinue()
{
  stream_ << " \\" << std::endl;
}

}  // namespace upf
